Skip to content

说说react中的 styled-components

在react项目中写CSS大致有以下三种方式:内联方式、引入样式类、使用styled-components。

jsx
<div style={{color: '#f3623d', fontSize: 14}}>Menu</div>
<div style={{color: '#f3623d', fontSize: 14}}>Menu</div>

以内联方式书写CSS代码,简单直接,适用于只设置一两个样式书写的情况,编译后生成的代码也是内联在HTML结点的style属性上。这种写法发缺陷是,一旦样式属性设置的过多,就会导致组件代码复杂,不易维护。当然也可以把样式对象从JSX中抽离出去,定义成一个单独的对象,或写在公共模块里,来解决该问题。这又会带来,一旦这样的样式对象过多,组件内部结点的样式优先级不清晰,且不易抽离成公共的样式模块,来供其它组件复用。

jsx
render() {
  return <span className="menu navigation-menu">Menu</span>
}
render() {
  return <span className="menu navigation-menu">Menu</span>
}

通过设置className属性,来给组件设置样式,是另一种常见的方式。对于需要抽离成公共样式的需求,可以定义在一个独立的样式类区块中,达到样式共享的目的,解决内联方式的弊端。样式类,可以定义在style中,也可以写在单独的样式文件中。借助于webpack构建工具的相关插件可以方便地集成less、sass等,做css的优雅降级。缺点是,静态样式类,有可能和页面中其它模块的命名冲突,导致某一处的样式类,污染了全局或其它模块的正常展示。当然,人为去定义一个不易导致冲突的样式类,可以在一定程度上缓解这个状况,但对于大型复杂的多人协作项目,这并不是一个靠谱的解决方案。

CSS
.diItemWrap {
  width: 100%;
  position: relative;
  display: inline-block;
  margin: 2px 10px 2px 0;
}
.diItemTitle {
  display: inline-block;
  text-align: right;
  padding-right: 10px;
  font-weight: 500;
  width: 120px;
}
.diItemWrap {
  width: 100%;
  position: relative;
  display: inline-block;
  margin: 2px 10px 2px 0;
}
.diItemTitle {
  display: inline-block;
  text-align: right;
  padding-right: 10px;
  font-weight: 500;
  width: 120px;
}
jsx
import styles from './styles.module.css';

return (
    <div className={styles.diItemWrap}>
      <span className={styles.diItemTitle}>
        {uiRequred}{title}
      </span>
    </div>
  );
import styles from './styles.module.css';

return (
    <div className={styles.diItemWrap}>
      <span className={styles.diItemTitle}>
        {uiRequred}{title}
      </span>
    </div>
  );

在项目工程中,借助于webpack的一些列CSS插件,可以实现在 JSX定义的组件中引用外部的样式类,并且最终注入的的样式类可以保证唯一性,不会引起命名冲突。但是不能很好地实现样式的继承,比如本例中只想在外层div标签引用一个样式类而同时控制外层div标签 和 里面的子元素是做不到的。

jsx
const Title = styled.h1`
  font-size: 1.5em;
  text-align: center;
  color: palevioletred;
`;
const Wrapper = styled.section`
  padding: 4em;
  background: papayawhip;
`;
render(
  <Wrapper>
    <Title>
      Hello World!
    </Title>
  </Wrapper>
);
const Title = styled.h1`
  font-size: 1.5em;
  text-align: center;
  color: palevioletred;
`;
const Wrapper = styled.section`
  padding: 4em;
  background: papayawhip;
`;
render(
  <Wrapper>
    <Title>
      Hello World!
    </Title>
  </Wrapper>
);

在日常的项目开发中使用styled-components(简称 SC),是做react项目时处理CSS最普遍的方式,它的优势很多,解决了之前在react中书写CSS的痛点,是一种CSS IN JS的成熟解决方案。

优势

styled-components的诞生初衷,致力于增强React组件中设置样式的效果,改善前端开发人员在JSX组件中书写CSS的体验,优化了最终的样式输出。

自动注入样式

styled-components 会自动追踪组件中的样式类,将页面中和组件关联的样式代码最终打包输出,没有引用到的无关样式代码自动忽略。这样结合代码拆分插件和异步组件,可以达到生成的代码量最少。

类名不会冲突

styled-components 给用户定义的样式类做增强处理,保证最终输出的样式类,在页面全局中保持唯一,轻松解决命名冲突的问题。

支持动态样式

根据传递的组件属性 或者 全局的主题,可以实现样式或主题的切换,无需手动维护。

其他优势

有了styled-components,在大型项目中可以轻松维护样式部分代码,无用的CSS自动移除,对于比较新的样式属性支持自动增加前缀来兼容低版本浏览器。

用法简介

安装步骤,自动跳过,使用 NPM 或 yarn 命令安装即可。

引入

jsx
import styled from 'styled-components';
import styled from 'styled-components';

普通用法

jsx
const Title = styled.h1`
  font-size: 1.5em;
  text-align: center;
  color: palevioletred;
`;
const Wrapper = styled.section`
  padding: 4em;
  background: papayawhip;
`;
render(
  <Wrapper>
    <Title>
      Hello World!
    </Title>
  </Wrapper>
);
const Title = styled.h1`
  font-size: 1.5em;
  text-align: center;
  color: palevioletred;
`;
const Wrapper = styled.section`
  padding: 4em;
  background: papayawhip;
`;
render(
  <Wrapper>
    <Title>
      Hello World!
    </Title>
  </Wrapper>
);

如上所示,定义了两个样式组件 Wrapper 和 Title,这两个业务组件自动绑定了一组样式。 最终生成的代码如下:

html
<style>
.cHGPOa {
    padding: 4em;
    background: papayawhip;
}
.ikxKmn {
    font-size: 1.5em;
    text-align: center;
    color: palevioletred;
}
</style>
<section class="sc-cBNfnY cHGPOa"><h1 class="sc-gWHgXt ikxKmn">Hello World!</h1></section>
<style>
.cHGPOa {
    padding: 4em;
    background: papayawhip;
}
.ikxKmn {
    font-size: 1.5em;
    text-align: center;
    color: palevioletred;
}
</style>
<section class="sc-cBNfnY cHGPOa"><h1 class="sc-gWHgXt ikxKmn">Hello World!</h1></section>

可以看到,最终输出的两个DOM结点section 和 h1 上自动添加了样式类,样式类关联了组件内设置的样式属性。

动态展示

jsx
const Lable = styled.span`
  font-size: ${props => props.bigger ? "16px" : "12px"};
`;
render(
  <div>
    <Lable>正常</Lable>
    <Lable bigger>大号</Lable>
  </div>
);
const Lable = styled.span`
  font-size: ${props => props.bigger ? "16px" : "12px"};
`;
render(
  <div>
    <Lable>正常</Lable>
    <Lable bigger>大号</Lable>
  </div>
);

通过在组件的模板字符串中,传递插值函数,根据属性参数来动态改变展示效果。

样式继承

jsx
const Button = styled.button`
  color: #fe3df6;
  font-size: 1em;
  margin: 1em;
  padding: 0.25em 1em;
`;

const RoundedButton = styled(Button)`
  border: 2px solid #fe3df6;
  border-radius: 3px;
`;

render(
    <div>
        <Button>正常按钮</Button>
        <RoundedButton>Tomato Button</RoundedButton>
    </div>
);
const Button = styled.button`
  color: #fe3df6;
  font-size: 1em;
  margin: 1em;
  padding: 0.25em 1em;
`;

const RoundedButton = styled(Button)`
  border: 2px solid #fe3df6;
  border-radius: 3px;
`;

render(
    <div>
        <Button>正常按钮</Button>
        <RoundedButton>Tomato Button</RoundedButton>
    </div>
);

首先用普通写法定义一个样式组件Button,通过将样式组件Button包裹在styled模板字符串中,实现对Button组件的样式继承。

装饰组件

jsx
const Link = ({ className, children }) => (
  <a className={className}>
    {children}
  </a>
);

const StyledLink = styled(Link)`
  color: palevioletred;
  font-weight: bold;
`;

render(
  <div>
    <Link>普通链接</Link>
    <br />
    <StyledLink>装饰后的链接</StyledLink>
  </div>
);
const Link = ({ className, children }) => (
  <a className={className}>
    {children}
  </a>
);

const StyledLink = styled(Link)`
  color: palevioletred;
  font-weight: bold;
`;

render(
  <div>
    <Link>普通链接</Link>
    <br />
    <StyledLink>装饰后的链接</StyledLink>
  </div>
);

styled 方法可以作用在自定义 或 第三方组件上,只要组件接收的className属性绑定在了DOM结点上。

传递属性

jsx
const Input = styled.input`
  padding: 0.5em;
  margin: 0.5em;
  color: ${props => props.inputColor || "red"};
  background: papayawhip;
  border: none;
  border-radius: 3px;
`;

render(
  <div>
    <Input defaultValue="红色字体" type="text" />
    <Input defaultValue="绿色字体" type="text" inputColor="green" />
  </div>
);
const Input = styled.input`
  padding: 0.5em;
  margin: 0.5em;
  color: ${props => props.inputColor || "red"};
  background: papayawhip;
  border: none;
  border-radius: 3px;
`;

render(
  <div>
    <Input defaultValue="红色字体" type="text" />
    <Input defaultValue="绿色字体" type="text" inputColor="green" />
  </div>
);

针对普通的DOM结点,传递已知的HTML属性;针对一个React组件,可以传递任何属性。如上面的代码所示,inputColor属性不会作用到原始的input DOM结点上,用于供上层组件Input来做样式的个性化展示。

附加属性

jsx
const Input = styled.input.attrs(props => ({
  type: "text",
  size: props.size || "1em",
}))`
  color: palevioletred;
  font-size: 1em;
  border: 2px solid palevioletred;
  border-radius: 3px;

  /* 使用上面定义好的动态计算属性 size */
  margin: ${props => props.size};
  padding: ${props => props.size};
`;

render(
  <div>
    <Input placeholder="小输入框" />
    <br />
    <Input placeholder="大输入框" size="2em" />
  </div>
);
const Input = styled.input.attrs(props => ({
  type: "text",
  size: props.size || "1em",
}))`
  color: palevioletred;
  font-size: 1em;
  border: 2px solid palevioletred;
  border-radius: 3px;

  /* 使用上面定义好的动态计算属性 size */
  margin: ${props => props.size};
  padding: ${props => props.size};
`;

render(
  <div>
    <Input placeholder="小输入框" />
    <br />
    <Input placeholder="大输入框" size="2em" />
  </div>
);

动画支持

jsx
const rotate = keyframes`
  from {
    transform: rotate(0deg);
  }

  to {
    transform: rotate(360deg);
  }
`;

// 下面创建一个可以在两秒内 旋转一次的组件
const Rotate = styled.div`
  display: inline-block;
  animation: ${rotate} 2s linear infinite;
  padding: 2rem 1rem;
  font-size: 1.2rem;
`;

render(
  <Rotate>&lt; 🐔 &gt;</Rotate>
);
const rotate = keyframes`
  from {
    transform: rotate(0deg);
  }

  to {
    transform: rotate(360deg);
  }
`;

// 下面创建一个可以在两秒内 旋转一次的组件
const Rotate = styled.div`
  display: inline-block;
  animation: ${rotate} 2s linear infinite;
  padding: 2rem 1rem;
  font-size: 1.2rem;
`;

render(
  <Rotate>&lt; 🐔 &gt;</Rotate>
);

CSS @keyframes 动画并不局限在单个组件中,为了避免命名冲突而污染全局,可以使用 styled-components 中提供的 keyframes 关键字,来创建唯一的动画实例(即保证 rotage的独一无二) 更多用法请移步至文末的官方文档地址

实现原理

在了解 styled-components 之前需要先温习一下 ES6中的 Tagged Template Literals(带标签的模板字符串)

javascript
var person = 'Mike';
var age = 28;
function myTag(strings, personExp, ageExp) {
  var str0 = strings[0]; // "that "
  var str1 = strings[1]; // " is a "
  var ageStr;
  if (ageExp > 99){
    ageStr = 'centenarian';
  } else {
    ageStr = 'youngster';
  }

  return str0 + personExp + str1 + ageStr;
}
var output = myTag`that ${ person } is a ${ age }`;
console.log(output);
// that Mike is a youngster
var person = 'Mike';
var age = 28;
function myTag(strings, personExp, ageExp) {
  var str0 = strings[0]; // "that "
  var str1 = strings[1]; // " is a "
  var ageStr;
  if (ageExp > 99){
    ageStr = 'centenarian';
  } else {
    ageStr = 'youngster';
  }

  return str0 + personExp + str1 + ageStr;
}
var output = myTag`that ${ person } is a ${ age }`;
console.log(output);
// that Mike is a youngster

如上面代码所示,myTag作为模板字符串的标签,可以视作一个函数,第一个参数包含一个字符串值的数组,其余的参数则一一关联到模板中的表达式。 同理,针对 使用styled-components定义的样式组件。

javascript
const Title = styled.h1`
  font-size: 1.5em;
`;
// 等价于
const Title = styled.h1('font-size: 1.5em;')
const Title = styled.h1`
  font-size: 1.5em;
`;
// 等价于
const Title = styled.h1('font-size: 1.5em;')

首先,引入styled-components时 底层会维护一个计数器counter,每生成一个组件实例,数值就会发生改变,再结合hash函数以及前缀等方式,生成唯一的组件ID。

javascript
counter++;
const componentId = 'sc-' + hash('sc' + counter);
counter++;
const componentId = 'sc-' + hash('sc' + counter);

接下来,我们简单分析一下 styled-components 的核心源码(这里我们使用当前最新版本的main分支),入口文件位于[ styled-components/packages/styled-components/src/constructors/styled.ts ]。

typescript
import createStyledComponent from '../models/StyledComponent';
import { IStyledComponentFactory, WebTarget } from '../types';
import domElements from '../utils/domElements';
import constructWithOptions from './constructWithOptions';

const styled = <Props>(tag: WebTarget) =>
  constructWithOptions<IStyledComponentFactory, Props>(createStyledComponent, tag);

type BaseStyled = typeof styled;

const enhancedStyled = styled as BaseStyled &
  {
    [key in typeof domElements[number]]: ReturnType<BaseStyled>;
  };

// 针对所有有效的 HTML 元素,支持快捷访问
// 即在项目中我们经常使用的 styled.button  等价于  styled('button')
domElements.forEach(domElement => {
  enhancedStyled[domElement] = styled(domElement);
});

export default enhancedStyled;
import createStyledComponent from '../models/StyledComponent';
import { IStyledComponentFactory, WebTarget } from '../types';
import domElements from '../utils/domElements';
import constructWithOptions from './constructWithOptions';

const styled = <Props>(tag: WebTarget) =>
  constructWithOptions<IStyledComponentFactory, Props>(createStyledComponent, tag);

type BaseStyled = typeof styled;

const enhancedStyled = styled as BaseStyled &
  {
    [key in typeof domElements[number]]: ReturnType<BaseStyled>;
  };

// 针对所有有效的 HTML 元素,支持快捷访问
// 即在项目中我们经常使用的 styled.button  等价于  styled('button')
domElements.forEach(domElement => {
  enhancedStyled[domElement] = styled(domElement);
});

export default enhancedStyled;

styled 函数接受一个DOM结点 或者 组件作为入参,返回 constructWithOptions 函数的执行结果。constructWithOptions.ts 位于同级目录下面,总共有 50 多行代码。

typescript
export default function constructWithOptions<
  Constructor extends Function = IStyledComponentFactory,
  OuterProps = undefined // used for styled<{}>().attrs() so attrs() gets the generic prop context
>(componentConstructor: Constructor, tag: WebTarget, options: Options = EMPTY_OBJECT as Object) {
  // We trust that the tag is a valid component as long as it isn't falsish
  // Typically the tag here is a string or function (i.e. class or pure function component)
  // However a component may also be an object if it uses another utility, e.g. React.memo
  // React will output an appropriate warning however if the `tag` isn't valid
  if (!tag) {
    throw styledError(1, tag);
  }

  /* This is callable directly as a template function */
  const templateFunction = <Props = OuterProps>(
    initialStyles: TemplateStringsArray | StyledObject | StyleFunction<Props>,
    ...interpolations: Interpolation<Props>[]
  ) => componentConstructor(tag, options, css(initialStyles, ...interpolations));

  /* Modify/inject new props at runtime */
  templateFunction.attrs = <Props = OuterProps>(attrs: Attrs<Props>) =>
    constructWithOptions<Constructor, Props>(componentConstructor, tag, {
      ...options,
      attrs: Array.prototype.concat(options.attrs, attrs).filter(Boolean),
    });

  /**
   * If config methods are called, wrap up a new template function and merge options */
  templateFunction.withConfig = (config: Options) =>
    constructWithOptions<Constructor, OuterProps>(componentConstructor, tag, {
      ...options,
      ...config,
    });

  return templateFunction;
}
export default function constructWithOptions<
  Constructor extends Function = IStyledComponentFactory,
  OuterProps = undefined // used for styled<{}>().attrs() so attrs() gets the generic prop context
>(componentConstructor: Constructor, tag: WebTarget, options: Options = EMPTY_OBJECT as Object) {
  // We trust that the tag is a valid component as long as it isn't falsish
  // Typically the tag here is a string or function (i.e. class or pure function component)
  // However a component may also be an object if it uses another utility, e.g. React.memo
  // React will output an appropriate warning however if the `tag` isn't valid
  if (!tag) {
    throw styledError(1, tag);
  }

  /* This is callable directly as a template function */
  const templateFunction = <Props = OuterProps>(
    initialStyles: TemplateStringsArray | StyledObject | StyleFunction<Props>,
    ...interpolations: Interpolation<Props>[]
  ) => componentConstructor(tag, options, css(initialStyles, ...interpolations));

  /* Modify/inject new props at runtime */
  templateFunction.attrs = <Props = OuterProps>(attrs: Attrs<Props>) =>
    constructWithOptions<Constructor, Props>(componentConstructor, tag, {
      ...options,
      attrs: Array.prototype.concat(options.attrs, attrs).filter(Boolean),
    });

  /**
   * If config methods are called, wrap up a new template function and merge options */
  templateFunction.withConfig = (config: Options) =>
    constructWithOptions<Constructor, OuterProps>(componentConstructor, tag, {
      ...options,
      ...config,
    });

  return templateFunction;
}

templateFunction 函数把传入的 args 作为 css 内容,由 css-helper 生成为 style-sheet,然后调用 componentConstructor 生成一个SC实例。 而这里的 componentConstructor 函数是【styled-components/packages/styled-components/src/models/StyledComponent.ts】 文件默认导出的。主要是根据 tag 和 css 生成styled-component。

typescript
// 利用 forwardRef 创建一个新的临时组件,而不是拓展父级组件
let WrappedStyledComponent: IStyledComponent;

function forwardRef(props: ExtensibleObject, ref: Ref<Element>) {
// eslint-disable-next-line
return useStyledComponentImpl(WrappedStyledComponent, props, ref, isStatic);
}

forwardRef.displayName = displayName;

WrappedStyledComponent = React.forwardRef(forwardRef) as unknown as IStyledComponent;
WrappedStyledComponent.attrs = finalAttrs;
WrappedStyledComponent.componentStyle = componentStyle;
WrappedStyledComponent.displayName = displayName;
WrappedStyledComponent.shouldForwardProp = shouldForwardProp;

// this static is used to preserve the cascade of static classes for component selector
// purposes; this is especially important with usage of the css prop
WrappedStyledComponent.foldedComponentIds = isTargetStyledComp
? styledComponentTarget.foldedComponentIds.concat(styledComponentTarget.styledComponentId)
: (EMPTY_ARRAY as string[]);

WrappedStyledComponent.styledComponentId = styledComponentId;

// fold the underlying StyledComponent target up since we folded the styles
WrappedStyledComponent.target = isTargetStyledComp ? styledComponentTarget.target : target;

// 生成样式类名
const generatedClassName = useInjectedStyle(
    componentStyle,
    isStatic,
    context,
    process.env.NODE_ENV !== 'production' ? forwardedComponent.warnTooManyClasses : undefined
);

// 生成结点
propsForElement[
    // handle custom elements which React doesn't properly alias
    isTargetTag &&
    domElements.indexOf(elementToBeCreated as unknown as Extract<typeof domElements, string>) === -1
      ? 'class'
      : 'className'
  ] = (foldedComponentIds as string[])
    .concat(
      styledComponentId,
      (generatedClassName !== styledComponentId ? generatedClassName : null) as string,
      props.className,
      attrs.className
    )
    .filter(Boolean)
    .join(' ');

  propsForElement.ref = refToForward;

  return createElement(elementToBeCreated, propsForElement);
// 利用 forwardRef 创建一个新的临时组件,而不是拓展父级组件
let WrappedStyledComponent: IStyledComponent;

function forwardRef(props: ExtensibleObject, ref: Ref<Element>) {
// eslint-disable-next-line
return useStyledComponentImpl(WrappedStyledComponent, props, ref, isStatic);
}

forwardRef.displayName = displayName;

WrappedStyledComponent = React.forwardRef(forwardRef) as unknown as IStyledComponent;
WrappedStyledComponent.attrs = finalAttrs;
WrappedStyledComponent.componentStyle = componentStyle;
WrappedStyledComponent.displayName = displayName;
WrappedStyledComponent.shouldForwardProp = shouldForwardProp;

// this static is used to preserve the cascade of static classes for component selector
// purposes; this is especially important with usage of the css prop
WrappedStyledComponent.foldedComponentIds = isTargetStyledComp
? styledComponentTarget.foldedComponentIds.concat(styledComponentTarget.styledComponentId)
: (EMPTY_ARRAY as string[]);

WrappedStyledComponent.styledComponentId = styledComponentId;

// fold the underlying StyledComponent target up since we folded the styles
WrappedStyledComponent.target = isTargetStyledComp ? styledComponentTarget.target : target;

// 生成样式类名
const generatedClassName = useInjectedStyle(
    componentStyle,
    isStatic,
    context,
    process.env.NODE_ENV !== 'production' ? forwardedComponent.warnTooManyClasses : undefined
);

// 生成结点
propsForElement[
    // handle custom elements which React doesn't properly alias
    isTargetTag &&
    domElements.indexOf(elementToBeCreated as unknown as Extract<typeof domElements, string>) === -1
      ? 'class'
      : 'className'
  ] = (foldedComponentIds as string[])
    .concat(
      styledComponentId,
      (generatedClassName !== styledComponentId ? generatedClassName : null) as string,
      props.className,
      attrs.className
    )
    .filter(Boolean)
    .join(' ');

  propsForElement.ref = refToForward;

  return createElement(elementToBeCreated, propsForElement);

如上所示,我们选取了StyledComponent.ts中的关键代码片段。首先根据传入的参数创建了一个临时包装组件,然后经过对 target 属性的处理和组合,生成关系样式表的动态样式类,最后实例化组件。 总结一下:

  • 我们利用 es6中的标签模板字符串,可以获得样式代码,以及传递的属性值等信息
  • 传递 target 参数,经过多层预处理和 组合,包装成一个临时组件
  • 生产样式类数组,并关联样式代码
  • 最后,将组件实例化,交给 react

如果 target 也是SC,则包含了自己和target的样式类名,起到了继承的效果

高级拓展

主题切换

jsx
import styled, { ThemeProvider } from 'styled-components';
const Item = styled.div`
	color: ${( props ) => props.theme.color.primary}
`
const theme = {
  color: {
    primary: 'red'
  }
}

class StyledComponentsDemo extends Component { 
  render() {
    return(
      <ThemeProvider theme={theme}>
	      <Item>Item 1</Item>
      </ThemeProvider>
    )
  }
}
import styled, { ThemeProvider } from 'styled-components';
const Item = styled.div`
	color: ${( props ) => props.theme.color.primary}
`
const theme = {
  color: {
    primary: 'red'
  }
}

class StyledComponentsDemo extends Component { 
  render() {
    return(
      <ThemeProvider theme={theme}>
	      <Item>Item 1</Item>
      </ThemeProvider>
    )
  }
}

通过暴露的ThemeProvider传递theme属性,实现不同主题效果的切换。普通的组件也可以通过提供的 withTheme 函数来访问主题属性

jsx
import { withTheme } from 'styled-components';
class MyComponent extends React.Component {
  render() {
    return <p>{this.props.theme.color.primary}</p>
  }
}
export default withTheme(MyComponent);
import { withTheme } from 'styled-components';
class MyComponent extends React.Component {
  render() {
    return <p>{this.props.theme.color.primary}</p>
  }
}
export default withTheme(MyComponent);

VS-Code插件

在VS-Code中书写样式组件的CSS代码时,默认没有提示,且没有样式属性的高亮展示,这一点不是很友好。 为了获得更好的代码书写体验,提升开发效率,这里推荐一个 VS-Code插件:vscode-styled-components。它支持 styled-components 的高亮展示 和 CSS语法智能提示。 效果对比很明显。

相关参考

https://styled-components.com/https://github.com/styled-components/styled-componentshttps://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Template_literalshttps://rangle.io/blog/styled-components-styled-systems-and-how-they-work/https://juejin.cn/post/6844904196425121800https://github.com/wangpin34/blog/issues/49

友情推荐

m-debug